﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Data.EntityClient;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Linq;
using System.Linq.Expressions;
using BrasilFramework.Comum;
using BrasilFramework.Comum.Excecao;
using BrasilFramework.Dado.Comum;
using BrasilFramework.Dado.EF.Comum;

namespace BrasilFramework.Dado.EF
{
    /// <summary>
    /// Classe base de Dado a ser herdada por toda classe de acesso a dados da aplicação que seja vinculada à uma classe de modelo
    /// </summary>
    public abstract class DadoBase<T> where T : EntityObject
    {
        #region Variáveis Privadas

        private readonly Conexao conexao;
        private readonly ObjectContext contexto;

        private readonly bool controlarTransacao = true;
        private readonly ObjectSet<T> modelo;

        #endregion

        #region Construtores

        /// <summary>
        /// Construtor padrão
        /// </summary>
        protected DadoBase()
            : this(null, null)
        {
        }

        /// <summary>
        /// Construtor padrão
        /// </summary>
        protected DadoBase(ObjectContext contexto)
            : this(null, contexto)
        {
        }

        /// <summary>
        /// Construtor Padrão
        /// </summary>
        protected DadoBase(Conexao.ControleTransacao controleTransacao)
            : this(null, null)
        {
            controlarTransacao = controleTransacao != Conexao.ControleTransacao.Local;
        }

        /// <summary>
        /// Construtor privado
        /// </summary>
        /// <param name="transacao">Objeto de transação</param>
        protected DadoBase(Transacao transacao)
        {
            conexao = transacao != null ? transacao.Conexao(Configuracao.Sigla) : new Conexao(Configuracao.Sigla);
            contexto = Mapeamento.Contexto(conexao);
            modelo = contexto.CreateObjectSet<T>(typeof(T).Name);
        }

        /// <summary>
        /// Construtor privado
        /// </summary>
        /// <param name="transacao">Objeto de transação</param>
        /// <param name="contexto">Contexto</param>
        protected DadoBase(Transacao transacao, ObjectContext contexto)
        {
            if (contexto == null)
            {
                conexao = transacao != null ? transacao.Conexao(Configuracao.Sigla) : new Conexao(Configuracao.Sigla);
                contexto = Mapeamento.Contexto(conexao);
            }
            else
                this.contexto = contexto;

            modelo = contexto.CreateObjectSet<T>(typeof(T).Name);
            modelo.MergeOption = MergeOption.NoTracking;
        }

        #endregion

        #region Métodos de Inserção, Alteração e Exclusão

        /// <summary>
        /// Insere um objeto
        /// </summary>
        /// <param name="objeto">Objeto a ser inserido</param>
        public virtual T Inserir(T objeto)
        {
            try
            {
                //Clonagem utilizada para evitar que o Entity comece a rastrear o objeto de origem.
                T objetoClonado = BrasilFramework.Comum.Utilitario.Objeto.Clonar(objeto);

                //Anexo e Desanexo para remover todos os objetos relacionados, senão o Entity tentará incluí-los.
                modelo.Attach(objetoClonado);
                modelo.Detach(objetoClonado);

                modelo.AddObject(objetoClonado);
                contexto.SaveChanges();
                return objetoClonado;
            }
            catch (Exception e)
            {
                DadoException excecao = new InserirException(e);

                if (excecao.Numero > 50000)
                    throw new DadoException(excecao.InnerException.Message, excecao);

                throw excecao;
            }
            finally
            {
                FecharConexao();
            }
        }

        /// <summary>
        /// Insere um objeto
        /// </summary>
        /// <param name="lista">Lista de objetos a serem inseridos</param>
        public virtual void InserirLista(IEnumerable<T> lista)
        {
            try
            {
                //Clonagem utilizada para evitar que o Entity comece a rastrear o objeto de origem.
                foreach (T objetoClonado in lista.Select(BrasilFramework.Comum.Utilitario.Objeto.Clonar))
                {
                    //Anexo e Desanexo para remover todos os objetos relacionados, pois o Entity tenta incluí-los.
                    modelo.Attach(objetoClonado);
                    modelo.Detach(objetoClonado);
                    modelo.AddObject(objetoClonado);
                }

                contexto.SaveChanges();
            }
            catch (Exception e)
            {
                DadoException excecao = new InserirException(e);

                if (excecao.Numero > 50000)
                    throw new DadoException(excecao.InnerException.Message, excecao);

                throw excecao;
            }
            finally
            {
                FecharConexao();
            }
        }

        /// <summary>
        /// Altera um objeto
        /// </summary>
        /// <param name="objeto">Objeto a ser alterado</param>
        public virtual void Alterar(T objeto)
        {
            try
            {
                //Clonagem utilizada para evitar que o Entity comece a rastrear o objeto de origem.
                T objetoClonado = BrasilFramework.Comum.Utilitario.Objeto.Clonar(objeto);
                modelo.Attach(objetoClonado);
                contexto.ObjectStateManager.ChangeObjectState(objetoClonado, EntityState.Modified);
                modelo.ApplyCurrentValues(objetoClonado);
                contexto.SaveChanges();
            }
            catch (Exception e)
            {
                DadoException excecao = new AlterarException(e);

                if (excecao.Numero > 50000)
                    throw new DadoException(excecao.InnerException.Message, excecao);

                throw excecao;
            }
            finally
            {
                FecharConexao();
            }

        }

        /// <summary>
        /// Altera um objeto
        /// </summary>
        /// <param name="lista">Lista de objetos a serem alterados</param>
        public virtual void AlterarLista(IEnumerable<T> lista)
        {
            try
            {
                //Clonagem utilizada para evitar que o Entity comece a rastrear o objeto de origem.
                foreach (T objetoClonado in lista.Select(BrasilFramework.Comum.Utilitario.Objeto.Clonar))
                {
                    modelo.Attach(objetoClonado);
                    contexto.ObjectStateManager.ChangeObjectState(objetoClonado, EntityState.Modified);
                    modelo.ApplyCurrentValues(objetoClonado);
                }

                contexto.SaveChanges();
            }
            catch (Exception e)
            {
                DadoException excecao = new AlterarException(e);

                if (excecao.Numero > 50000)
                    throw new DadoException(excecao.InnerException.Message, excecao);

                throw excecao;
            }
            finally
            {
                FecharConexao();
            }

        }

        /// <summary>
        /// Exclui um objeto
        /// </summary>
        /// <param name="objeto">Objeto a ser excluido</param>
        public virtual void Excluir(T objeto)
        {
            try
            {
                //Clonagem utilizada para evitar que o Entity comece a rastrear o objeto de origem.
                T objetoClonado = BrasilFramework.Comum.Utilitario.Objeto.Clonar(objeto);
                modelo.Attach(objetoClonado);
                modelo.DeleteObject(objetoClonado);
                contexto.SaveChanges();
            }
            catch (Exception e)
            {
                DadoException excecao = new ExcluirException(e);

                if (excecao.Numero > 50000)
                    throw new DadoException(excecao.InnerException.Message, excecao);

                if (excecao.Numero == 547)
                    throw new DadoException(
                        "Exclusão não realizada. Existem outras informações que dependem desse registro.", excecao);

                throw excecao;
            }
            finally
            {
                FecharConexao();
            }
        }

        /// <summary>
        /// Exclui lista de objetos
        /// </summary>
        /// <param name="lista">Lista de objetos a serem excluídos</param>
        public virtual void ExcluirLista(IEnumerable<T> lista)
        {
            try
            {
                //Clonagem utilizada para evitar que o Entity comece a rastrear o objeto de origem.
                foreach (T objetoClonado in lista.Select(BrasilFramework.Comum.Utilitario.Objeto.Clonar))
                {
                    modelo.Attach(objetoClonado);
                    modelo.DeleteObject(objetoClonado);
                }

                contexto.SaveChanges();
            }
            catch (Exception e)
            {
                DadoException excecao = new ExcluirException(e);

                if (excecao.Numero > 50000)
                    throw new DadoException(excecao.InnerException.Message, excecao);

                if (excecao.Numero == 547)
                    throw new DadoException(
                        "Exclusão não realizada. Existem outras informações que dependem desse registro.", excecao);

                throw excecao;
            }
            finally
            {
                FecharConexao();
            }
        }

        #endregion

        #region Métodos de Consulta

        /// <summary>
        /// Consulta uma lista de objetos
        /// </summary>
        /// <typeparam name="T">Tipo do Objeto</typeparam>
        /// <param name="listaNavegacao">Inclui os objetos da navegação informada no resultado da consulta.</param>
        /// <param name="quantidade">Quantidade de objetos que deverá ser retornado.</param>
        /// <returns>Lista de objetos</returns>
        public virtual List<T> ConsultarLista(string[] listaNavegacao = null, int quantidade = 0)
        {
            return ConsultarLista(null, listaNavegacao, quantidade);
        }

        /// <summary>
        /// Consulta uma lista de objetos
        /// </summary>
        /// <typeparam name="T">Tipo do Objeto</typeparam>
        /// <param name="quantidade">Quantidade de objetos que deverá ser retornado.</param>
        /// <returns>Lista de objetos</returns>
        public virtual List<T> ConsultarLista(int quantidade)
        {
            return ConsultarLista(item => item, null, null, quantidade);
        }

        /// <summary>
        /// Consulta uma lista de objetos
        /// </summary>
        /// <typeparam name="T">Tipo do Objeto</typeparam>
        /// <param name="condicao">Condição da pesquisa.</param>
        /// <param name="quantidade">Quantidade de objetos que deverá ser retornado.</param>
        /// <returns>Lista de objetos</returns>
        public virtual List<T> ConsultarLista(Expression<Func<T, bool>> condicao, int quantidade)
        {
            return ConsultarLista(item => item, condicao, null, quantidade);
        }

        /// <summary>
        /// Consulta uma lista de objetos
        /// </summary>
        /// <typeparam name="T">Tipo do Objeto</typeparam>
        /// <param name="condicao">Condição da pesquisa.</param>
        /// <param name="navegacao">Inclui os objetos da navegação informada no resultado da consulta.</param>
        /// <param name="quantidade">Quantidade de objetos que deverá ser retornado.</param>
        /// <returns>Lista de objetos</returns>
        public virtual List<T> ConsultarLista(Expression<Func<T, bool>> condicao, string navegacao, int quantidade = 0)
        {
            return ConsultarLista(item => item, condicao, navegacao != null ? new[] { navegacao } : null, quantidade);
        }

        /// <summary>
        /// Consulta uma lista de objetos
        /// </summary>
        /// <typeparam name="T">Tipo do Objeto</typeparam>
        /// <param name="condicao">Condição da pesquisa.</param>
        /// <param name="listaNavegacao">Inclui os objetos da navegação informada no resultado da consulta.</param>
        /// <param name="quantidade">Quantidade de objetos que deverá ser retornado.</param>
        /// <returns>Lista de objetos</returns>
        public virtual List<T> ConsultarLista(Expression<Func<T, bool>> condicao, string[] listaNavegacao = null, int quantidade = 0)
        {
            return ConsultarLista(item => item, condicao, listaNavegacao, quantidade);
        }

        /// <summary>
        /// Consulta uma lista de objetos
        /// </summary>
        /// <typeparam name="T">Tipo do Objeto</typeparam>
        /// <typeparam name="R">Tipo do resultado do objeto.</typeparam>
        /// <param name="colunas">Expressão de seleção do objeto.</param>
        /// <param name="condicao">Condição da pesquisa.</param>
        /// <param name="listaNavegacao">Inclui os objetos da navegação informada no resultado da consulta.</param>
        /// <param name="quantidade">Quantidade de objetos que deverá ser retornado.</param>
        /// <returns>Lista de objetos</returns>
        public virtual List<R> ConsultarLista<R>(Expression<Func<T, R>> colunas,
                                                 Expression<Func<T, bool>> condicao, string[] listaNavegacao = null, int quantidade = 0)
        {
            try
            {
                if (condicao == null) condicao = item => true;

                ObjectQuery<T> consulta = modelo;

                if (listaNavegacao != null)
                    consulta = listaNavegacao.Aggregate(consulta, (consultaAtual, navegacao) => consultaAtual.Include(navegacao));

                return quantidade > 0 ? consulta.Where(condicao).Select(colunas).Take(quantidade).ToList() : consulta.Where(condicao).Select(colunas).ToList();
            }
            catch (Exception e)
            {
                throw new ConsultarException(e);
            }
            finally
            {
                FecharConexao();
            }
        }

        /// <summary>
        /// Consulta um único objeto
        /// </summary>
        /// <typeparam name="T">Tipo do Objeto</typeparam>
        /// <param name="navegacao">Inclui os objetos da navegação informada no resultado da consulta.</param>
        /// <returns>Objeto</returns>
        public T ConsultarObjeto(string navegacao = null)
        {
            return ConsultarObjeto(null, navegacao);
        }

        /// <summary>
        /// Consulta um único objeto
        /// </summary>
        /// <typeparam name="T">Tipo do Objeto</typeparam>
        /// <param name="condicao">Condição para consulta.</param>
        /// <param name="navegacao">Inclui os objetos da navegação informada no resultado da consulta.</param>
        /// <returns>Objeto</returns>
        public T ConsultarObjeto(Expression<Func<T, bool>> condicao, string navegacao = null)
        {
            return ConsultarObjeto(condicao, navegacao != null ? new[] { navegacao } : null);
        }

        /// <summary>
        /// Consulta um único objeto
        /// </summary>
        /// <typeparam name="T">Tipo do Objeto</typeparam>
        /// <param name="condicao">Condição para consulta.</param>
        /// <param name="listaNavegacao">Inclui os objetos da navegação informada no resultado da consulta.</param>
        /// <returns>Objeto</returns>
        public T ConsultarObjeto(Expression<Func<T, bool>> condicao, string[] listaNavegacao)
        {
            try
            {
                if (condicao == null) condicao = T => true;

                ObjectQuery<T> consulta = modelo;

                if (listaNavegacao != null)
                    consulta = listaNavegacao.Aggregate(consulta, (consultaAtual, navegacao) => consultaAtual.Include(navegacao));

                return consulta.FirstOrDefault(condicao);
            }
            catch (Exception e)
            {
                throw new ConsultarException(e);
            }
            finally
            {
                FecharConexao();
            }
        }

        #endregion

        #region Métodos básicos

        /// <summary>
        /// Executa uma procedure e não retorna resultado
        /// </summary>
        /// <param name="nome">Nome da procedure mapeada</param>
        /// <param name="parametros">Coleção de parâmetros</param>
        protected void ExecutarProcedureNonQuery(string nome, ColecaoParametro parametros)
        {
            try
            {
                contexto.ExecuteStoreCommand(nome, ConverterListaParametro(parametros));
            }
            catch (Exception e)
            {
                throw new ExecutarProcedureException(e);
            }
            finally
            {
                FecharConexao();
            }
        }


        /// <summary>
        /// Executa uma procedure e retorna um scalar
        /// </summary>
        /// <param name="nome">Nome da procedure mapeada</param>
        /// <param name="parametros">Coleção de parâmetros</param>
        /// <returns>Scalar</returns>
        protected TR ExecutarProcedureScalar<TR>(string nome, ColecaoParametro parametros)
        {
            return contexto.ExecuteStoreQuery<TR>(nome, ConverterListaParametro(parametros)).FirstOrDefault();
        }

        /// <summary>
        /// Executa uma procedure e retorna uma lista
        /// </summary>
        /// <param name="nome">Nome da procedure mapeada</param>
        /// <param name="parametros">Coleção de parâmetros</param>
        /// <returns>Lista de objetos</returns>
        protected List<T> ExecutarProcedureLista(string nome, ColecaoParametro parametros)
        {
            try
            {
                return contexto.ExecuteStoreQuery<T>(nome, ConverterListaParametro(parametros)).ToList();
            }
            catch (Exception e)
            {
                throw new ExecutarProcedureException(e);
            }
            finally
            {
                FecharConexao();
            }
        }

        private void FecharConexao()
        {
            if (conexao != null && controlarTransacao && conexao.ConexaoBase.State == ConnectionState.Open)
                conexao.Fechar();
        }

        #endregion

        private static IEnumerable<EntityParameter> ConverterListaParametro(ColecaoParametro parametros)
        {
            return parametros.Select(item => new EntityParameter { ParameterName = "@" + item.Key, Value = item.Value });
        }
    }

    /// <summary>
    /// Exceção ao Inserir
    /// </summary>
    public class InserirException : DadoException
    {
        public InserirException()
            : this(null)
        {
        }

        public InserirException(Exception innerException)
            : base("Erro ao tentar inserir objeto no banco de dados.", innerException)
        {
        }
    }

    /// <summary>
    /// Exceção ao Alterar
    /// </summary>
    public class AlterarException : DadoException
    {
        public AlterarException()
            : this(null)
        {
        }

        public AlterarException(Exception innerException)
            : base("Erro ao tentar alterar objeto no banco de dados.", innerException)
        {
        }
    }

    /// <summary>
    /// Exceção ao Excluir
    /// </summary>
    public class ExcluirException : DadoException
    {
        public ExcluirException()
            : this(null)
        {
        }

        public ExcluirException(Exception innerException)
            : base("Erro ao tentar excluir objeto no banco de dados.", innerException)
        {
        }
    }

    /// <summary>
    /// Exceção ao Consultar
    /// </summary>
    public class ConsultarException : DadoException
    {
        public ConsultarException()
            : this(null)
        {
        }

        public ConsultarException(Exception innerException)
            : base("Erro ao tentar consultar objeto(s) no banco de dados.", innerException)
        {
        }
    }

    /// <summary>
    /// Exceção ao Executar Procedure
    /// </summary>
    public class ExecutarProcedureException : DadoException
    {
        public ExecutarProcedureException()
            : this(null)
        {
        }

        public ExecutarProcedureException(Exception innerException)
            : base("Erro ao tentar executar procedure no banco de dados.", innerException)
        {
        }
    }

    /// <summary>
    /// Exceção ao tentar preencher propriedade diferente do tipo IList<T />
    /// </summary>
    public class ColecaoNaoSuportadaException : DadoException
    {
        public ColecaoNaoSuportadaException()
            : this(null)
        {
        }

        public ColecaoNaoSuportadaException(Exception innerException)
            : base("Erro ao tentar preencher coleção. Coleção não suportada pelo Estrutura V2.", innerException)
        {
        }
    }

    /// <summary>
    /// Exceção ao tentar atribuir valor NULL a um parâmetro NOT NULL
    /// </summary>
    public class ParametroNotNullException : DadoException
    {
        public ParametroNotNullException(string nomeParametro)
            : this(null, nomeParametro)
        {
        }

        public ParametroNotNullException(Exception innerException, string nomeParametro)
            : base(
                string.Format("Erro ao tentar atribuir um valor NULL a um parâmetro NOT NULL. Parâmetro: {0}",
                              nomeParametro), innerException)
        {
        }
    }
}